/*jshint esversion: 6 */
let base32 = require('./base32.js'),
    CryptoJS = require('./CryptoJS.js'),
    Buffer = require('./Buffer.js');

/**
 * Digest the one-time passcode options.
 *
 * @param {Object} options
 * @param {String} options.secret Shared secret key
 * @param {Integer} options.counter Counter value
 * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
 *   base32, base64).
 * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
 *   sha512).
 * @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
 *   Shared secret key
 * @return {Buffer} The one-time passcode as a buffer.
 */

function isset(v) {
    return (v !== undefined) && (v !== null);
}

exports.digest = function digest(options) {
    let i;

    // unpack options
    let secret = options.secret;
    let counter = options.counter;
    let encoding = options.encoding || 'ascii';
    let algorithm = (options.algorithm || 'sha1').toLowerCase();

    // convert secret to buffer
    if (!Buffer.isBuffer(secret)) {
        secret = (encoding === 'base32') ? base32.decode(secret) : new Buffer(secret, encoding);
    }
    // let secret_buffer_size;
    // if (algorithm === 'sha1') {
    //     secret_buffer_size = 20; // 20 bytes
    // } else if (algorithm === 'sha256') {
    //     secret_buffer_size = 32; // 32 bytes
    // } else if (algorithm === 'sha512') {
    //     secret_buffer_size = 64; // 64 bytes
    // } else {
    //     console.warn('Speakeasy - The algorithm provided (`' + algorithm + '`) is not officially supported, results may be different than expected.');
    // }
    // The secret for sha1, sha256 and sha512 needs to be a fixed number of bytes for the one-time-password to be calculated correctly
    // Pad the buffer to the correct size be repeating the secret to the desired length
    // if (secret_buffer_size && secret.length !== secret_buffer_size) {
    //     secret = new Buffer(Array(Math.ceil(secret_buffer_size / secret.length) + 1).join(secret.toString('hex')), 'hex').slice(0, secret_buffer_size);
    // }

    // create an buffer from the counter
    let buf = new Buffer(8);
    let tmp = counter;
    for (i = 0; i < 8; i++) {
        // mask 0xff over number to get last 8
        buf[7 - i] = tmp & 0xff;

        // shift 8 and get ready to loop over the next batch of 8
        tmp = tmp >> 8;
    }
    // init hmac with the key
    let hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo[algorithm.toUpperCase()], CryptoJS.enc.Hex.parse(secret.toString('hex')));

    // update hmac with the counter
    hmac.update(CryptoJS.enc.Hex.parse(buf.toString('hex')));

    // return the digest
    return new Buffer(hmac.finalize().toString(CryptoJS.enc.Hex), 'hex');
};

/**
 * Generate a counter-based one-time token. Specify the key and counter, and
 * receive the one-time password for that counter position as a string. You can
 * also specify a token length, as well as the encoding (ASCII, hexadecimal, or
 * base32) and the hashing algorithm to use (SHA1, SHA256, SHA512).
 *
 * @param {Object} options
 * @param {String} options.secret Shared secret key
 * @param {Integer} options.counter Counter value
 * @param {Buffer} [options.digest] Digest, automatically generated by default
 * @param {Integer} [options.digits=6] The number of digits for the one-time
 *   passcode.
 * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
 *   base32, base64).
 * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
 *   sha512).
 * @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
 *   Shared secret key
 * @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The
 *   number of digits for the one-time passcode.
 * @return {String} The one-time passcode.
 */

exports.hotp = function hotpGenerate(options) {

    // verify secret and counter exists
    let secret = options.secret;
    let counter = options.counter;

    if (!isset(secret)) {
        throw new Error('Speakeasy - hotp - Missing secret');
    }
    if (!isset(counter)) {
        throw new Error('Speakeasy - hotp - Missing counter');
    }

    // unpack digits
    // backward compatibility: `length` is also accepted here, but deprecated
    let digits = (isset(options.digits) ? options.digits : 6);

    // digest the options
    let digest = options.digest || exports.digest(options);

    // compute HOTP offset
    let offset = (digest[digest.length - 1] & 0xf);

    // calculate binary code (RFC4226 5.4)
    let code = ((digest[offset] & 0x7f) << 24) | ((digest[offset + 1] & 0xff) << 16) | ((digest[offset + 2] & 0xff) << 8) | (digest[offset + 3] & 0xff);

    // left-pad code
    code = new Array(digits + 1).join('0') + code.toString(10);

    // return length number off digits
    return code.substr(-digits);
};

// Alias counter() for hotp()
exports.counter = exports.hotp;

/**
 * Calculate counter value based on given options. A counter value converts a
 * TOTP time into a counter value by finding the number of time steps that have
 * passed since the epoch to the current time.
 *
 * @param {Object} options
 * @param {Integer} [options.time] Time in seconds with which to calculate
 *   counter value. Defaults to `Date.now()`.
 * @param {Integer} [options.step=30] Time step in seconds
 * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
 *   which to calculate the counter value. Defaults to 0 (no offset).
 * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.)
 *   Initial time in seconds since the UNIX epoch from which to calculate the
 *   counter value. Defaults to 0 (no offset).
 * @return {Integer} The calculated counter value.
 * @private
 */

exports._counter = function _counter(options) {
    let step = options.step || 30;
    let time = isset(options.time) ? (options.time * 1000) : Date.now();

    // also accepts 'initial_time', but deprecated
    let epoch = (isset(options.epoch) ? (options.epoch * 1000) : 0);
    return Math.floor((time - epoch) / step / 1000);
};

/**
 * Generate a time-based one-time token. Specify the key, and receive the
 * one-time password for that time as a string. By default, it uses the current
 * time and a time step of 30 seconds, so there is a new token every 30 seconds.
 * You may override the time step and epoch for custom timing. You can also
 * specify a token length, as well as the encoding (ASCII, hexadecimal, or
 * base32) and the hashing algorithm to use (SHA1, SHA256, SHA512).
 *
 * Under the hood, TOTP calculates the counter value by finding how many time
 * steps have passed since the epoch, and calls HOTP with that counter value.
 *
 * @param {Object} options
 * @param {String} options.secret Shared secret key
 * @param {Integer} [options.time] Time in seconds with which to calculate
 *   counter value. Defaults to `Date.now()`.
 * @param {Integer} [options.step=30] Time step in seconds
 * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
 *   epoch from which to calculate the counter value. Defaults to 0 (no offset).
 * @param {Integer} [options.counter] Counter value, calculated by default.
 * @param {Integer} [options.digits=6] The number of digits for the one-time
 *   passcode.
 * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
 *   base32, base64).
 * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
 *   sha512).
 * @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
 *   Shared secret key
 * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.)
 *   Initial time in seconds since the UNIX epoch from which to calculate the
 *   counter value. Defaults to 0 (no offset).
 * @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The
 *   number of digits for the one-time passcode.
 * @return {String} The one-time passcode.
 */

exports.totp = function totpGenerate(options) {
    // shadow options
    options = Object.create(options);

    // verify secret exists if key is not specified
    let secret = options.secret;
    if (!isset(secret)) {
        throw new Error('Speakeasy - totp - Missing secret');
    }
    // calculate default counter value
    if (!isset(options.counter)) {
        options.counter = exports._counter(options);
    }
    // pass to hotp
    return this.hotp(options);
};

// Alias time() for totp()
exports.time = exports.totp;